﻿//-----------------------------------------------------------------------
// <copyright file="KeyboardInputHandler.cs" author="Artak Mkrtchyan">
//     Creative Commons - GNU General Public License (http://creativecommons.org/licenses/GPL/2.0/
// </copyright>
// <author>Artak Mkrtchyan</author>
// <email>mkArtak@gmail.com</email>
// <date>05/05/2013</date>
//-----------------------------------------------------------------------

using ConsoleHoster.Common.Utilities;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;

namespace ConsoleHoster.Common.UserInput
{
	public sealed class KeyboardInputHandler : IKeyboardInputHandler
	{
		private const string LogMessageFormat = "{0} for key: '{1}' in context: '{2}'";
		private const string LogErrorFormat = "{0} for key: '{1}' in context: '{2}'. Reason: '{3}'";

		private readonly IDictionary<Key, IList<KeyboardEventHandler>> registeredHandlers = new ConcurrentDictionary<Key, IList<KeyboardEventHandler>>();
		private readonly IKeyboardStateProvider keyboardStateProvider;
		private readonly ILogger logger;

		public KeyboardInputHandler(IKeyboardStateProvider argKeyboardStateProvider, ILogger argLogger)
		{
			ValidationManager.RequireArgumentNotNull(argKeyboardStateProvider, "argKeyboardStateProvider");

			this.keyboardStateProvider = argKeyboardStateProvider;
			this.logger = new ComponentLogger("KeyboardInputHandler", argLogger);
		}

		public void RegisterHandler(Key argHitKey, IEnumerable<Key> argHeldKeys, string argContext, Func<IKeyboardStateProvider, bool> argHandler)
		{
			this.LogMessage("Registering hotkey handler", argHitKey, argContext);

			try
			{
				KeyboardEventHandler tmpHandler = new KeyboardEventHandler(argContext, argHitKey, argHeldKeys, argHandler);

				IList<KeyboardEventHandler> tmpRegisteredHandlers;

				if (this.registeredHandlers.TryGetValue(argHitKey, out tmpRegisteredHandlers) && tmpRegisteredHandlers.Contains(tmpHandler))
				{
					throw new ApplicationException(String.Format("There is already hotkey handler handler registered for the key: '{0}' with context: '{1}'", argHitKey, argContext));
				}

				if (tmpRegisteredHandlers == null)
				{
					tmpRegisteredHandlers = new List<KeyboardEventHandler>();
					this.registeredHandlers.Add(argHitKey, tmpRegisteredHandlers);
				}

				tmpRegisteredHandlers.Add(tmpHandler);
				this.LogMessage("KIH registration succeeded", argHitKey, argContext);
			}
			catch (Exception ex)
			{
				this.logger.LogError(LogErrorFormat, "Failed to unregister handler", argHitKey.ToString(), argContext, ex.ToString());
			}
		}

		public bool UnregisterHandler(Key argHitKey, IEnumerable<Key> argHeldKeys, string argContext)
		{
			this.LogMessage("Unregistering hotkey handler", argHitKey, argContext);

			bool tmpResult = false;

			try
			{
				IList<KeyboardEventHandler> tmpRegisteredHandlers;
				if (this.registeredHandlers.TryGetValue(argHitKey, out tmpRegisteredHandlers) && tmpRegisteredHandlers != null)
				{
					// Using fake handler here, as the handler parameter isn't involved in the comparison - hence has no effect
					KeyboardEventHandler tmpHandler = new KeyboardEventHandler(argContext, argHitKey, argHeldKeys, kih => true);
					if (tmpRegisteredHandlers.Contains(tmpHandler))
					{
						this.LogMessage("Removing found registered handler", argHitKey, argContext);
						tmpRegisteredHandlers.Remove(tmpHandler);
						tmpResult = true;
						this.LogMessage("Handler removal succeeded", argHitKey, argContext);
					}
				}
			}
			catch (Exception ex)
			{
				this.logger.LogError(LogErrorFormat, "Failed to unregister handler", argHitKey.ToString(), argContext, ex.ToString());
			}

			return tmpResult;
		}

		public bool HandleKeyboardInput(Key argHitKey, string argContext)
		{
			KeyboardEventHandler tmpHandler = null;

			IList<KeyboardEventHandler> tmpList;
			if (this.registeredHandlers.TryGetValue(argHitKey, out tmpList))
			{
				foreach (KeyboardEventHandler tmpCurrentHandler in tmpList.Where(item => item.Context == argContext))
				{
					if ((tmpCurrentHandler.HeldKeys == null && this.NoSpecialKeysHeld()) || (tmpCurrentHandler.HeldKeys != null && this.keyboardStateProvider.AllKeysDown(tmpCurrentHandler.HeldKeys)))
					{
						tmpHandler = tmpCurrentHandler;
						this.LogMessage("Registered hotkey found", argHitKey, argContext);
						break;
					}
				}
			}

			bool tmpHandled = false;
			if (tmpHandler != null)
			{
				this.LogMessage("Calling registered handler", argHitKey, argContext);
				try
				{
					tmpHandled = tmpHandler.Handler(this.keyboardStateProvider);
				}
				catch (Exception ex)
				{
					this.logger.LogError(
						"Exception occured while calling registered handler for key: '{0}' in context: '{1}'. Error details are: {2}",
						argHitKey.ToString(),
						argContext,
						ex.ToString());
					tmpHandled = true;
				}
				this.LogMessage("Handler call succeeded", argHitKey, argContext);
			}

			return tmpHandled;
		}

		private bool NoSpecialKeysHeld()
		{
			return !this.keyboardStateProvider.AnyOfKeysDown(new[] { Key.LeftCtrl, Key.RightCtrl, Key.LeftShift, Key.RightShift, Key.LeftAlt, Key.RightAlt });
		}

		private void LogMessage(string argMessage, Key argHitKey, string argContext)
		{
			this.logger.LogMessage(LogMessageFormat, argMessage, argHitKey.ToString(), argContext);
		}

		private sealed class KeyboardEventHandler
		{
			private readonly string context;
			private readonly Func<IKeyboardStateProvider, bool> handler;
			private readonly Key hitKey;
			private readonly ReadOnlyCollection<Key> heldKeys;
			private readonly Lazy<int> hashCode;

			private static bool ContainSameItems(ReadOnlyCollection<Key> argItems1, ReadOnlyCollection<Key> argItems2)
			{
				if (argItems1 == null && argItems2 == null)
				{
					return true;
				}

				bool tmpResult = false;

				if (argItems1 != null && argItems2 != null)
				{
					tmpResult = argItems1.Count == argItems2.Count && argItems1.Any(item => argItems2.Contains(item));
				}

				return tmpResult;
			}

			public KeyboardEventHandler(string argContext, Key argHitKey, IEnumerable<Key> argHeldKeysKeys, Func<IKeyboardStateProvider, bool> argHandler)
			{
				ValidationManager.RequireArgumentNotNull(argHandler, "argHandler");

				this.context = argContext;
				if (argHeldKeysKeys != null)
				{
					this.heldKeys = new ReadOnlyCollection<Key>(argHeldKeysKeys.ToList());
				}
				this.handler = argHandler;
				this.hitKey = argHitKey;

				this.hashCode = new Lazy<int>(() =>
					{
						int tmpHashCode = (int)this.HitKey * 100000;

						if (this.HeldKeys != null)
						{
							foreach (Key k in this.HeldKeys)
							{
								tmpHashCode += (int)k;
							}
						}

						if (this.Context != null)
						{
							tmpHashCode ^= this.Context.GetHashCode();
						}

						return tmpHashCode;
					});
			}

			public override int GetHashCode()
			{
				return this.hashCode.Value;
			}

			public override bool Equals(object obj)
			{
				return this.Equals(obj as KeyboardEventHandler);
			}

			private bool Equals(KeyboardEventHandler argComparee)
			{
				bool tmpResult = false;

				if (argComparee != null)
				{
					tmpResult = this.Context == argComparee.Context && this.HitKey == argComparee.HitKey && ContainSameItems(this.HeldKeys, argComparee.HeldKeys);
				}

				return tmpResult;
			}

			public string Context
			{
				get
				{
					return this.context;
				}
			}

			public Func<IKeyboardStateProvider, bool> Handler
			{
				get
				{
					return this.handler;
				}
			}

			public Key HitKey
			{
				get
				{
					return this.hitKey;
				}
			}

			public ReadOnlyCollection<Key> HeldKeys
			{
				get
				{
					return this.heldKeys;
				}
			}
		}
	}
}
